package com.senlin.ethereum.core;


import com.senlin.ethereum.common.Constant;
import com.senlin.ethereum.exception.EthSendException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.web3j.crypto.*;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.Transfer;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 *
 * geth --datadir private-geth --rpc --rpcaddr "0.0.0.0" --rpcport "8545"
 * --rpccorsdomain "*" --rpcapi "db,eth,net,web3,personal" console
 *
 *
 *  ！当你用一些以太币Ether创建了一个有效的帐户时，你可以使用两种机制来与以太坊进行交易。
 *  1、通过以太坊ethereum客户端进行认证签名交易
 *  2、离线交易签名认证
 *
 * @author huwei
 * @since 1.0
 */
@Slf4j
public final class EthClient {
    private static Web3j web3 = null;
    private final String ethNodeAddress;
    private static String walletKeystore;
    private final EthEventLoop ethEventLoop;

    private EthClient(String ethNodeAddress) {
        this.ethNodeAddress = ethNodeAddress;
        this.ethEventLoop =  EthEventLoop.current();
        walletKeystore = this.defaultWalletKeystore();
        web3 = Web3j.build(new HttpService(ethNodeAddress));
    }
    public static EthClient me(String... nodes) {
        if (nodes.length <= 0) return new EthClient("http://localhost:8545/");
        return new EthClient(nodes[0]);
    }
    public EthWallet ethWallet(EthAccount account) {
        return EthWallet.current(account);
    }
    public EthWallet ethWallet(File file) { return EthWallet.current(file); }
    public EthWallet ethWallet(String privateKey) { return EthWallet.current(privateKey); }
    public EthWallet ethWallet(ECKeyPair ecKeyPair) { return EthWallet.current(ecKeyPair); }


    public EthClient walletKeystore(String path) {
        if(StringUtils.isBlank(path)) return this;
        walletKeystore = path;
        return this;
    }
    private String defaultWalletKeystore() {
        if (System.getProperty("os.name").toLowerCase().startsWith("win"))
            return Constant.DEFAULT_WIN_ETH_KEYSTORE;
        else
            return Constant.DEFAULT_LINUX_ETH_KEYSTORE;
    }

    public static class EthWallet {
        private final Credentials credentials;
        private static final ThreadLocal<BigInteger> NonceLocal = new ThreadLocal<>();

        private EthWallet(Credentials credentials) {
            this.credentials = credentials;
        }
        private EthWallet(EthAccount ethAccount) {
            this.credentials = this.loadCredentials(ethAccount.getMemorizingWords());
        }
        private EthWallet(File file) {
            this.credentials = this.loadCredentials(file);
        }

        private static EthWallet current(EthAccount ethAccount) {
            if (ethAccount == null || StringUtils.isBlank(ethAccount.getMemorizingWords()))
                throw new IllegalArgumentException(Constant.EthErrorPrint.header + "EthAccount为Null, 或者助记词为空!");
            return new EthWallet(ethAccount);
        }
        private static EthWallet current(File file) {
            return new EthWallet(file);
        }
        public static EthWallet current(String privateKey) {
            Credentials credentials = Credentials.create(privateKey);
            return new EthWallet(credentials);
        }
        public static EthWallet current(ECKeyPair ecKeyPair) {
            Credentials credentials = Credentials.create(ecKeyPair);
            return new EthWallet(credentials);
        }

        private Credentials loadCredentials(String memorizingWords) {
            Credentials credentials = WalletUtils.loadBip39Credentials("", memorizingWords);
            if (credentials == null)
                throw new IllegalStateException(Constant.EthErrorPrint.header + "加载钱包失败！请检查助记词");
            return credentials;
        }
        private Credentials loadCredentials(File file) {
            try {
                Credentials credentials = WalletUtils.loadCredentials("", file);
                if (credentials == null) throw new CipherException("");
                return credentials;
            } catch (IOException | CipherException e) {
                throw new IllegalStateException(Constant.EthErrorPrint.header + "加载钱包出错: " + e.getMessage());
            }
        }


        /**
         * 1、获取指定钱包地址的账户余额, 返回BigDecimal类型的数字，
         * 单位为 ether。
         * 2、同步方式获取。
         *
         * @return Optional<BigDecimal>
         */
        public Optional<BigDecimal> getBalance(Convert.Unit... units) {
            String address = this.credentials.getAddress();
            if (!WalletUtils.isValidAddress(address))
                throw new IllegalArgumentException(Constant.EthErrorPrint.header + "无效钱包地址");

    /*        try {
                // 指定默认的区块参数，建议选最新区块
                // HEX String - 一个整数块号
                // String "earliest" - 为最早/起源块
                // String "latest" - 为最新的采矿块
                // String "pending" - 待处理状态/交易
                EthGetBalance ethBalance = EthClient.web3.ethGetBalance(address,
                        DefaultBlockParameterName.LATEST).sendAsync().get();
                // wei -> default is ether
                Convert.Unit unit = units.length > 0 ? units[0] : Convert.Unit.ETHER;
                BigDecimal ether = Convert.fromWei(ethBalance.getBalance().toString(), unit);
                return Optional.of(ether);

            } catch (InterruptedException e) {
            } catch (ExecutionException e) {
                log.error("{}获取Balance出错！钱包地址为：{}", Constant.EthErrorPrint.header, this.credentials.getAddress(), e);
            }
            return Optional.empty();*/
            return EthClient.geBalance(web3, address, units);
        }

        /**
         * 发送一笔交易
         * @param rawTransactionWrapper
         * @param consumer
         */
        public void transferTo(RawTransactionWrapper rawTransactionWrapper, Consumer<String> consumer, Consumer<Exception> except) {
            if (consumer == null)
                throw new IllegalArgumentException(Constant.EthErrorPrint.header + "Consumer不能为null，你必须处理这次消费！");
            if (rawTransactionWrapper == null)
                throw new IllegalArgumentException(Constant.EthErrorPrint.header + "你必须创建一个RawTransactionWrapper来生成一笔交易！");

            try {
                // 获取交易随机数nonce
                BigInteger nonce = this.genericTransactionNonce();
                EthWallet.NonceLocal.set(nonce);
                log.info(Constant.EthErrorPrint.header + "nonce = {}", nonce);
                // 创建交易 <nonce gasPrice gasLimit toAddress, data>
                RawTransaction rawTransaction = rawTransactionWrapper.build();
                if (rawTransaction == null)
                    throw new IllegalStateException(Constant.EthErrorPrint.header + "RawTransaction不能为null!请确定RawTransactionWrapper创建交易是否失败。");
                // 发送交易
                String transactionHash = this.sendTransaction(rawTransaction);
                consumer.accept(transactionHash);

            } catch (InterruptedException e) {
            } catch (ExecutionException | IOException | EthSendException e) {
                log.error("{}交易出错，交易发起地址{}！", Constant.EthErrorPrint.header, this.credentials.getAddress(), e);
                except.accept(e);
            } finally {
                EthWallet.NonceLocal.remove();
            }
        }

        /**
         * 发起一笔交易
         * !!!注意 => 默认希望发送到目的地的以太币数量 -> BigInteger.ZERO(0个)
         *
         * ***当在Ethereum以太坊发生交易时，必须为执行该交易的客户端支付交易成本，
         * 将该交易的输出提交到以太坊区块链Ethereum blockchain。
         *  gasPrice 气体价格，这是每单位gas中以太的消耗量。
         *                 Web3j使用的默认价格为22000000000 wei(22×10-8 Ether)。
         *                 这是在交易管理中定义的。
         *  gasLimit 气体最大量，这是你愿意在交易执行上花费的gas的最大总量。
         *                 单个交易在一个以太坊区块中有多大的上限，通常将该值限制为小于6700000。
         *                 当前的gas限制在这里查https://ethstats.net/。
         * => 这两个参数共同决定了你愿意花费在交易成本上的最大量的以太币Ether。
         *   也就是说，你花费的gas不会超过gas price * gas limit。
         *   gas价格也会影响交易发生的速度，这取决于其他交易是否能为矿工提供更有利的gas价格。
         *
         *  toAddress 目的地钱包地址
         *  data 0x<hex encoded text>
         * @return
         */
        public static class RawTransactionBuilder {
            private BigInteger nonce;
            private BigInteger gasPrice;
            private BigInteger gasLimit;
            private String to;
            private BigInteger value = BigInteger.ZERO;
            private String data;
            private String init;

            private volatile int mark = -1;

            public RawTransactionBuilder(String to) {
                this(DefaultGasProvider.GAS_PRICE, DefaultGasProvider.GAS_LIMIT, to);
            }
            public RawTransactionBuilder(BigInteger gasPrice, BigInteger gasLimit, String to) {
                if (!WalletUtils.isValidAddress(to))
                    throw new IllegalStateException(Constant.EthErrorPrint.header + "目的钱包无效！");
                this.nonce = EthWallet.NonceLocal.get();
                this.gasPrice = gasPrice;
                this.gasLimit = gasLimit;
                this.to = to;
            }
            public RawTransactionBuilder value(BigInteger value) {
                this.value = value;
                return this;
            }
            public RawTransactionBuilder dataTransaction(String data) {
                this.checkAndSet(0);
                this.data = Numeric.toHexString(data.getBytes());
                return this;
            }
            public RawTransactionBuilder contractTransaction(String init) {
                this.checkAndSet(1);
                this.init = init;
                return this;
            }

            private synchronized void checkAndSet(int setMark) {
                if (mark != -1) throw new IllegalStateException(Constant.EthErrorPrint.header + "dataTransaction/contractTransaction 方法不能同时存在！");
                else this.mark = setMark;
            }

            public RawTransaction build() {
                log.info(Constant.EthErrorPrint.header + "again nonce = {}", nonce);
                switch (this.mark) {
                    case -1:
                        log.info("{}创建一笔以太币交易 (:", Constant.EthErrorPrint.header);
                        return RawTransaction.createEtherTransaction(this.nonce, this.gasPrice, this.gasLimit, this.to, this.value);

                    case 0:
                        log.info("{}创建一笔存证交易 (:", Constant.EthErrorPrint.header);
                        return RawTransaction.createTransaction(this.nonce, this.gasPrice, this.gasLimit, this.to, this.value, this.data);

                    case 1:
                        log.info("{}创建一笔智能合约交易 (:", Constant.EthErrorPrint.header);
                        return RawTransaction.createContractTransaction(this.nonce, this.gasPrice, this.gasLimit, this.value, this.init);

                    default:
                        throw new IllegalStateException(Constant.EthErrorPrint.header + "创建交易失败！");
                }
            }
        }

        /**
         * 获取Nonce
         * @return
         */
        private BigInteger genericTransactionNonce() throws IOException {

            EthGetTransactionCount ethGetTransactionCount = web3.ethGetTransactionCount(
                    this.credentials.getAddress(), DefaultBlockParameterName.LATEST).send();
            if (ethGetTransactionCount.hasError())
                throw new IllegalStateException(ethGetTransactionCount.getError().getMessage());
            BigInteger nonce = ethGetTransactionCount.getTransactionCount();
            return nonce;
        }

        /**
         * 发送交易
         * @param rawTransaction
         * @return
         * @throws ExecutionException
         * @throws InterruptedException
         */
        private String sendTransaction(RawTransaction rawTransaction) throws ExecutionException, InterruptedException, EthSendException {
            // 对交易进行签名与编码
            byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, this.credentials);
            String hexValue = Numeric.toHexString(signedMessage);
            // 发送交易
            EthSendTransaction ethSendTransaction = EthClient.web3.ethSendRawTransaction(hexValue).sendAsync().get();
            String transactionHash = ethSendTransaction.getTransactionHash();
            if (ethSendTransaction.hasError())
                throw new EthSendException(ethSendTransaction.getError().getMessage());
            if (transactionHash == null)
                throw new EthSendException("TransactionHash为Null，请检查jsonrpc");
            return transactionHash;
        }

        /**
         * 通过助记词加载钱包地址
         * @param credentialsGet
         * @return
         */
        public String loadCredentialsAndGet(CredentialsGet credentialsGet) {
            return credentialsGet.get(EthWallet.this.credentials);
        }
    }

    public String getEthNodeAddress() { return this.ethNodeAddress; }
    public Web3j web3() { return this.web3; }
    public String getWalletKeystore() {
        return walletKeystore;
    }

    public enum Status {
        OK, NETWORK_ERROR, ETH_NODE_FAILED;
    }


    /**
     * 探测eth节点状态
     * @return
     */
    public EthClient.Status ping() {
        try {
            Web3ClientVersion web3ClientVersion = EthClient.web3.web3ClientVersion().send();
            String clientVersion = web3ClientVersion.getWeb3ClientVersion();
            if (clientVersion == null || clientVersion.length() == 0)
                return Status.ETH_NODE_FAILED;
            return Status.OK;
        } catch (IOException e) {
            log.error("{}网络异常！", Constant.EthErrorPrint.header);
            return Status.NETWORK_ERROR;
        }
    }

    /**
     *  - Geth/v1.9.9-stable-01744997/linux-amd64/go1.13.5
     *  - EthereumJS TestRPC/v2.0.2/ethereum-js
     *
     *  -> 获取Ethereum的节点版本，同步方式。
     *
     * @return Optional<String>
     */
    public Optional<String> getEthNodeVersion() {
        try {
            Web3ClientVersion web3ClientVersion = web3.web3ClientVersion().sendAsync().get();
            String version = web3ClientVersion.getWeb3ClientVersion();
            return Optional.of(version);

        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
            log.error("{}获取EthNode版本出错！", Constant.EthErrorPrint.header);
        }
        return Optional.empty();
    }

    /**
     * 1、创建一个Eth账户，
     * 2、返回的EthAccount类型包括如下：
     *      - 12个单词的助记词
     *      - 钱包地址
     *      - 钱包文件的存储路径
     *      - 公钥与私钥
     *
     *  提醒！！！
     *  可以先生成N个账户存入redis或其他存储中提升每次获取账户的速度
     *
     * @return
     */
    public Optional<EthAccount> createAccount() {
        try {
            Path path = Paths.get(walletKeystore);
            this.checkAndCreateFolder(path);

            Bip39Wallet wallet = WalletUtils.generateBip39Wallet("", path.toFile());
            // 生成12个单词的助记词
            String memorizingWords = wallet.getMnemonic();
            // 通过钱包密码与助记词获得钱包地址、公钥及私钥信息
            Credentials credentials = WalletUtils.loadBip39Credentials("", memorizingWords);
            // 钱包地址
            String address = credentials.getAddress();
            // 公钥与私钥
            String publicKey = credentials.getEcKeyPair().getPublicKey().toString(16);
            String privateKey = credentials.getEcKeyPair().getPrivateKey().toString(16);
            // 生成钱包存储的绝对地址
            String absoluteWalletFilePath = walletKeystore + wallet.getFilename();
            log.info("{}AbsoluteWalletPath: {}", Constant.EthErrorPrint.header, absoluteWalletFilePath);
            // 构建自定义Eth账号，并返回
            EthAccount ethAccount = new EthAccount(memorizingWords, address, publicKey,
                                                        privateKey, absoluteWalletFilePath);
            return Optional.of(ethAccount);

        } catch (IOException | CipherException e) {
            log.error("{}Create Wallet Failed!", Constant.EthErrorPrint.header, e);
        }
        return Optional.empty();
    }

    private void checkAndCreateFolder(Path path) throws IOException {
        if (!Files.exists(path)) Files.createDirectories(path);
    }

    /**
     * 获取交易收据
     * @param transactionHashList
     */
    public void handleTransactionReceipt(List<String> transactionHashList, int delay, TimeUnit timeUnit,
                                         EthEvent<TransactionReceipt, String> ethEvent) {
        if (transactionHashList == null || transactionHashList.size() == 0)
            return;
        if (ethEvent == null)
            throw new IllegalStateException(Constant.EthErrorPrint.header + "EthEvent不能为null");
        if (delay < 0 || timeUnit == null)
            throw new IllegalArgumentException(Constant.EthErrorPrint.header + "请指定有效的延迟时间");

        // 循环执行以下操作，期望最终在完成交易后收到收据
        for (int i = 0; i < transactionHashList.size(); i ++) {
            final String transactionHash = transactionHashList.get(i);
            if (StringUtils.isBlank(transactionHash)) break;

            // 消费
            this.ethEventLoop.asyncConsume(() -> {
                try {
                    EthGetTransactionReceipt transactionReceipt = web3.ethGetTransactionReceipt(transactionHash).send();
                    if (!transactionReceipt.getTransactionReceipt().isPresent()) {
                        // 再试一次，无限次
                        ethEvent.onPending(transactionHash);
                    } else {
                        TransactionReceipt receipt = transactionReceipt.getTransactionReceipt().get();
                        if (receipt.isStatusOK()) ethEvent.onSuccess(receipt);
                        else ethEvent.onPending(transactionHash);
                    }
                } catch (IOException e) {
                    log.error("{}获取交易收据失败!", Constant.EthErrorPrint.header, e);
                    ethEvent.onExcept(transactionHash, e);
                }
            });

            try {
                timeUnit.sleep(delay);
            } catch (InterruptedException e) {}
        }
    }
    public void handleTransactionReceipt(List<String> transactionHashList,
                                         EthEvent<TransactionReceipt, String> ethEvent) {
        this.handleTransactionReceipt(transactionHashList, 1, TimeUnit.SECONDS, ethEvent);
    }
    public void handleTransactionReceipt(String transactionHash,
                                         EthEvent<TransactionReceipt, String> ethEvent) {
        this.handleTransactionReceipt(Arrays.asList(transactionHash), 0, TimeUnit.SECONDS, ethEvent);
    }
    public void handleTransactionReceipt(String transactionHash, int delay, TimeUnit timeUnit,
                                         EthEvent<TransactionReceipt, String> ethEvent) {
        this.handleTransactionReceipt(Arrays.asList(transactionHash), delay, timeUnit, ethEvent);
    }
    public void handleTransactionReceipt(Supplier<List<String>> supplier, int delay, TimeUnit timeUnit,
                                         EthEvent<TransactionReceipt, String> ethEvent) {
        if (supplier == null) throw new IllegalArgumentException(Constant.EthErrorPrint.header + "supplier不能为null");
        this.handleTransactionReceipt(supplier.get(), delay, timeUnit, ethEvent);
    }
    public void handleTransactionReceipt(Supplier<List<String>> supplier,
                                         EthEvent<TransactionReceipt, String> ethEvent) {
        this.handleTransactionReceipt(supplier, 1, TimeUnit.SECONDS, ethEvent);
    }

    /**
     * 获取交易详情
     * @param transactionHash
     * @param consumer
     */
    public void getTransactionDetails(String transactionHash, Consumer<org.web3j.protocol.core.methods.response.Transaction> consumer) throws ExecutionException {
        try {
            EthTransaction ethTransaction = web3.ethGetTransactionByHash(transactionHash).sendAsync().get();
            if (!ethTransaction.getTransaction().isPresent())
                throw new IllegalStateException(Constant.EthErrorPrint.header + "交易详情不存在，请检测交易Hash！");

            org.web3j.protocol.core.methods.response.Transaction transaction = ethTransaction.getTransaction().get();
            if (consumer != null) consumer.accept(transaction);
            else log.info("{}{}", Constant.EthErrorPrint.header, transaction.toString());

        } catch (InterruptedException e) {}
    }

    /**
     * 估算手续费上限
     * @param transaction
     * @return
     */
    public BigInteger requestCurrentGasLimit(Transaction transaction) throws IOException {
        EthEstimateGas ethEstimateGas = web3.ethEstimateGas(transaction).send();
        return ethEstimateGas.getAmountUsed();
    }

    /**
     * 获取当前Gas价格
     * @return
     * @throws IOException
     */
    public BigInteger requestCurrentGasPrice() throws IOException {
        EthGasPrice ethGasPrice = this.web3.ethGasPrice().send();
        return ethGasPrice.getGasPrice();
    }

    /**
     * 获取钱包账户的 balance
     * @param address
     * @param units
     * @return
     */
    private static Optional<BigDecimal> geBalance(Web3j web3, String address, Convert.Unit... units) {
        try {
            // 指定默认的区块参数，建议选最新区块
            // HEX String - 一个整数块号
            // String "earliest" - 为最早/起源块
            // String "latest" - 为最新的采矿块
            // String "pending" - 待处理状态/交易
            EthGetBalance ethGetBalance = web3.ethGetBalance(address,
                    DefaultBlockParameterName.LATEST).sendAsync().get();
            // default wei -> ether
            Convert.Unit unit = units.length > 0 ? units[0] : Convert.Unit.ETHER;
            BigDecimal ether = Convert.fromWei(ethGetBalance.getBalance().toString(), unit);
            return Optional.ofNullable(ether);
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
            log.error("{}获取Balance出错！钱包地址为：{}", Constant.EthErrorPrint.header, address, e);
        }
        return Optional.empty();
    }
    public Optional<BigDecimal> getBalance(String address, Convert.Unit... units) {
        return EthClient.geBalance(web3, address, units);
    }
}

